﻿#include<iostream>
#include<string>
#include<vector>
#include<fstream>
#include<sstream>

#include <glad/gl-headers.h>
#include <GLFW/glfw3.h>
#include "vmath.h"

#include "node.h"
#include "objectfactory.h"
#include "glslprogram.h"

#define MAX(x,y) x>y? x:y
#define MIN(x,y) x>y? y:x
struct RenderObject {
    vmath::mat4 modelMatrix;
    std::vector<vmath::vec4> vertices;
    std::vector<vmath::vec4> normals;
    std::vector<vmath::vec4> colors;
};

std::vector<std::vector<vmath::vec4>> generateSphere(std::size_t slices, std::size_t stacks,
                                                     float radius)
{
    const double PI = 3.141592653589793;
    const double sliceStep = 360.0 / (double)slices;
    const double stackStep = 180.0 / (double)stacks;
    std::vector<std::vector<vmath::vec4>> result;
    std::vector<vmath::vec4> result_v;
    std::vector<vmath::vec4> result_r;
    auto degToRad = [PI](double deg) {
        return (PI / 180.0) * deg;
    };
    auto pointOf_v = [degToRad, radius](double theta, double phi) {
        return vmath::vec4{ radius *(float)std::cos(degToRad(theta)) *(float)std::cos(degToRad(phi)),
                            radius *(float)std::cos(degToRad(theta)) *(float)std::sin(degToRad(phi)),
                            radius *(float)std::sin(degToRad(theta)), 1.0 };
    };
    auto pointOf_r = [degToRad](double theta, double phi) {
        return vmath::vec4{ (float)std::cos(degToRad(theta)) *(float)std::cos(degToRad(phi)),  //
                            (float)std::cos(degToRad(theta)) *(float)std::sin(degToRad(phi)),  //
                            (float)std::sin(degToRad(theta)), 1.0 };
    };
    for (std::size_t stack = 0; stack < stacks; ++stack) {
        double theta = -90.0 + stack * stackStep;
        double thetaNext = -90.0 + (stack + 1) * stackStep;
        for (std::size_t slice = 0; slice < slices; ++slice) {
            double phi = slice * sliceStep;
            double phiNext = (slice + 1) * sliceStep;
            result_v.push_back(pointOf_v(theta, phi));
            result_v.push_back(pointOf_v(theta, phiNext));
            result_v.push_back(pointOf_v(thetaNext, phi));
            result_v.push_back(pointOf_v(thetaNext, phi));
            result_v.push_back(pointOf_v(theta, phiNext));
            result_v.push_back(pointOf_v(thetaNext, phiNext));

            result_r.push_back(vmath::normalize(pointOf_r(theta, phi)));
            result_r.push_back(vmath::normalize(pointOf_r(theta, phiNext)));
            result_r.push_back(vmath::normalize(pointOf_r(thetaNext, phi)));
            result_r.push_back(vmath::normalize(pointOf_r(thetaNext, phi)));
            result_r.push_back(vmath::normalize(pointOf_r(theta, phiNext)));
            result_r.push_back(vmath::normalize(pointOf_r(thetaNext, phiNext)));
        }
    }
    result.push_back(result_v);
    result.push_back(result_r);
    return result;
}

vmath::mat4 convertProjectionToImage(const vmath::mat4 &matrix)
{
    vmath::vec4 a(0.5, 0.0, 0.0, 0.0);
    vmath::vec4 b(0.0, 0.5, 0.0, 0.0);
    vmath::vec4 c(0.0, 0.0, 0.5, 0.0);
    vmath::vec4 d(0.5, 0.5, 0.5, 1.0);
    static const vmath::mat4 bias(a, b, c, d);
    return bias * matrix;
}

class Shadowmap : public Node
{
public:
    Shadowmap();
protected:
    bool startup();
    void render(double currentTime, double difTime);
    void shutdown();
    void onResized(int width, int height)override;
private:
    void shadowProgram(double currentTime, double difTime);
    void renderProgram(double currentTime, double difTime);
private:
    GLSLProgram _shadowProgram;
    GLSLProgram _renderProgram;
private:
    vmath::mat4 renderMatrix;
    vmath::mat4 shadowMatrix;
    vmath::mat4 viewMatrix;
    vmath::mat4 projectionMatrix;
    const float kFoV = 45.5f;
    const float kAspectRatio = 800.0f / 600.0f;
    const float kUpdateTimeFactor = 0.5f;
    const vmath::vec3 kCameraPosition = { 0.0f, 5.0, -15.0 };
    const vmath::vec3 kCameraTarget = { 0.0f, 0.0f, 0.0f };
    const vmath::vec2 kClipDistances = { 1.0f, 2000.0f };
    const vmath::vec3 kLightDirection = { 1.0f, -0.5f, 0.5f };
    const vmath::vec3 kLightPosition = vmath::normalize(-kLightDirection) * 5.0f;
    struct suniFormData {
        std::vector<vmath::mat4> mvpData;
    };
    struct runiFormData {
        std::vector<vmath::mat4> mvpData;
        std::vector<vmath::mat4> depthMVPData;
    };
    std::vector<RenderObject> renderObjects;
    suniFormData Uniformdata_s;
    runiFormData Uniformdata_r;
    GLuint shadowmapFramebuffer;
    GLuint shadowmapTexture;
    GLuint allVao;
    GLuint allVbo;
    GLuint uboBlock_s;
    GLuint uboBlock_r;
};

Shadowmap::Shadowmap()
{
    nodeName = "Shadowmap";
}

void Shadowmap::onResized(int width, int height)
{
    glViewport(0, 0, width, height);
    projectionMatrix = vmath::perspective(45.0, width / float(height), kClipDistances[0],
                                           kClipDistances[1]);
    viewMatrix = vmath::lookat(kCameraPosition, kCameraTarget, vmath::vec3{ 0.0f, 1.0f, 0.0f });
    Node::onResized(width, height);
}

bool Shadowmap::startup()
{
    if (PerfWindow::glVersion < 330) {
        printf("Failed! The GL version is less than 3.3 !\n");
        return false;
    }
    const GLenum buf_n = GL_NONE;

    projectionMatrix = vmath::perspective(kFoV, kAspectRatio, kClipDistances[0], kClipDistances[1]);
    viewMatrix = vmath::lookat(kCameraPosition, kCameraTarget, vmath::vec3{ 0.0f, 1.0f, 0.0f });
    renderMatrix = projectionMatrix * viewMatrix;
    vmath::mat4 shadowProjectionMatrix = vmath::ortho(-30.0f, 30.0f, -15.0f, 15.0f, -30.0f, 30.0f);
    vmath::mat4 shadowViewMatrix = vmath::lookat(kLightPosition, vmath::vec3{ 0.0f }, vmath::vec3{ 0.0f, 1.0f, 0.0f });
    shadowMatrix = shadowProjectionMatrix * shadowViewMatrix;

    std::vector<vmath::vec4> normals = std::vector<vmath::vec4>(36);
    const std::vector<vmath::vec4> vertices = {
        { -0.5f, -0.5f, -0.5f, 1.0f }, { -0.5f, -0.5f, 0.5f, 1.0f }, { -0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, -0.5f, 1.0f }, { -0.5f, -0.5f, -0.5f, 1.0f }, { -0.5f, 0.5f, -0.5f, 1.0f },
        { 0.5f, -0.5f, 0.5f, 1.0f }, { -0.5f, -0.5f, -0.5f, 1.0f }, { 0.5f, -0.5f, -0.5f, 1.0f }, { 0.5f, 0.5f, -0.5f, 1.0f }, { 0.5f, -0.5f, -0.5f, 1.0f }, { -0.5f, -0.5f, -0.5f, 1.0f },
        { -0.5f, -0.5f, -0.5f, 1.0f }, { -0.5f, 0.5f, 0.5f, 1.0f }, { -0.5f, 0.5f, -0.5f, 1.0f }, { 0.5f, -0.5f, 0.5f, 1.0f }, { -0.5f, -0.5f, 0.5f, 1.0f }, { -0.5f, -0.5f, -0.5f, 1.0f },
        { -0.5f, 0.5f, 0.5f, 1.0f }, { -0.5f, -0.5f, 0.5f, 1.0f }, { 0.5f, -0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, -0.5f, -0.5f, 1.0f }, { 0.5f, 0.5f, -0.5f, 1.0f },
        { 0.5f, -0.5f, -0.5f, 1.0f }, { 0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, -0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, -0.5f, 1.0f }, { -0.5f, 0.5f, -0.5f, 1.0f },
        { 0.5f, 0.5f, 0.5f, 1.0f }, { -0.5f, 0.5f, -0.5f, 1.0f }, { -0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, 0.5f, 0.5f, 1.0f }, { -0.5f, 0.5f, 0.5f, 1.0f }, { 0.5f, -0.5f, 0.5f, 1.0f },
    };
    for (int x = -2; x < 2; ++x) {
        for (int y = -2; y < 2; ++y) {
            vmath::mat4 cubeModelMatrix;
            vmath::vec4 cubeColor = { 0.4f, 0.4f, 0.4f, 1.0f };
            if ((x + y) % 2) {
                cubeColor += vmath::vec4{ 0.1f, 0.1f, 0.1f, 0.0f };
            }
            vmath::mat4 scaleMatrix = vmath::scale(6.0f, 1.0f, 6.0f);
            vmath::mat4 translateMatrix = vmath::translate((float)x, -0.5f, (float)y);
            cubeModelMatrix = scaleMatrix * translateMatrix;
            renderObjects.push_back({ cubeModelMatrix, vertices, normals, std::vector<vmath::vec4>(vertices.size(), cubeColor) });
        }
    }
    for (int x = -6; x < 6; ++x) {
        for (int y = -6; y < 6; ++y) {
            vmath::mat4 cubeModelMatrix;
            vmath::vec4 cubeColor = { 0.25f, 0.25f, 0.25f, 1.0f };
            if (y % 2 == 0) {
                cubeColor = { 0.25f, 0.25f, 0.25f, 1.0f };
            } else {
                cubeColor = { 0.75f, 0.75f, 0.75f, 1.0f };
            }
            cubeModelMatrix = vmath::translate((float)(2.0f * x), 0.7f, (float)(2.0f * y));
            if ((x + y) % 2) {
                vmath::mat4 translateMatrix = vmath::translate<float>(0.0, 1.0, 0.0);
                cubeModelMatrix = cubeModelMatrix * translateMatrix;
            }
            renderObjects.push_back(
            { cubeModelMatrix, vertices, normals, std::vector<vmath::vec4>(vertices.size(), cubeColor) });
        }
    }
    std::vector<std::vector<vmath::vec4>> sphereGenerator_data = generateSphere(30, 30, 1.0f);
    vmath::mat4 sphereModelMatrix1, sphereModelMatrix2, sphereModelMatrix3, sphereModelMatrix4;
    vmath::vec4 sphereColor = { 0.2f, 0.2f, 0.2f, 1.0f };

    sphereModelMatrix1 = vmath::translate(4.0f, 3.5f, 0.0f);
    sphereModelMatrix2 = vmath::translate(-4.0f, 3.5f, 0.0f);
    sphereModelMatrix3 = vmath::translate(0.0f, 3.5f, 4.0f);
    sphereModelMatrix4 = vmath::translate(0.0f, 3.5f, -4.0f);

    sphereModelMatrix1 = sphereModelMatrix1 * vmath::scale(1.35f, 1.35f, 1.35f);
    sphereModelMatrix2 = sphereModelMatrix2 * vmath::scale(1.35f, 1.35f, 1.35f);
    sphereModelMatrix3 = sphereModelMatrix3 * vmath::scale(1.35f, 1.35f, 1.35f);
    sphereModelMatrix4 = sphereModelMatrix4 * vmath::scale(1.35f, 1.35f, 1.35f);
    renderObjects.push_back({ sphereModelMatrix1, sphereGenerator_data[0], sphereGenerator_data[1],
                               std::vector<vmath::vec4>(sphereGenerator_data[0].size(), sphereColor) });
    renderObjects.push_back({ sphereModelMatrix2, sphereGenerator_data[0], sphereGenerator_data[1],
                               std::vector<vmath::vec4>(sphereGenerator_data[0].size(), sphereColor) });
    renderObjects.push_back({ sphereModelMatrix3, sphereGenerator_data[0], sphereGenerator_data[1],
                               std::vector<vmath::vec4>(sphereGenerator_data[0].size(), sphereColor) });
    renderObjects.push_back({ sphereModelMatrix4, sphereGenerator_data[0], sphereGenerator_data[1],
                               std::vector<vmath::vec4>(sphereGenerator_data[0].size(), sphereColor) });
    glEnable(GL_DEPTH_TEST);
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

    glGenTextures(1, &shadowmapTexture);
    glBindTexture(GL_TEXTURE_2D, shadowmapTexture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    for (int i = 0; i <= 1; i++) {
        glTexImage2D(GL_TEXTURE_2D, i, GL_DEPTH_COMPONENT32F, 1000, 1000, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
    }

    glGenFramebuffers(1, &shadowmapFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, shadowmapFramebuffer);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowmapTexture, 0);

    glBindFramebuffer(GL_FRAMEBUFFER, shadowmapFramebuffer);
    glDrawBuffers(1, &buf_n);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    bool ok = _shadowProgram.build("../media/shaders/shadowmap/shadow.vert",
                                   "../media/shaders/shadowmap/shadow.frag");
    ok &= _renderProgram.build("../media/shaders/shadowmap/render.vert",
                                   "../media/shaders/shadowmap/render.frag");
    if (!ok) {
        return false;
    }

    glGenBuffers(1, &allVbo);
    std::vector<vmath::vec4> vboData;
    for (std::size_t index = 0; index < renderObjects.size(); ++index) {
        for (std::size_t index1 = 0; index1 < renderObjects[index].vertices.size(); ++index1) {
            vboData.push_back(renderObjects[index].vertices[index1]);
            vboData.push_back(renderObjects[index].normals[index1]);
            vboData.push_back(renderObjects[index].colors[index1]);
        }
    }
    GLvoid *vData = (GLvoid *)(vboData.data());
    GLsizeiptr size = sizeof(vmath::vec4) * vboData.size();
    glBindBuffer(GL_ARRAY_BUFFER, allVbo);
    glBufferData(GL_ARRAY_BUFFER, size, vData, GL_STATIC_DRAW);

    glGenVertexArrays(1, &allVao);
    glBindVertexArray(allVao);
    glBindBuffer(GL_ARRAY_BUFFER, allVbo);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 3 * sizeof(vmath::vec4), nullptr);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 3 * sizeof(vmath::vec4),
                          (const void *)sizeof(vmath::vec4));
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 3 * sizeof(vmath::vec4),
                          (const void *)(2 * sizeof(vmath::vec4)));
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    glGenBuffers(1, &uboBlock_s);
    for (const auto &renderObject : renderObjects) {
        vmath::mat4 MVP_s = shadowMatrix * renderObject.modelMatrix;
        vmath::mat4 MVP_r = renderMatrix * renderObject.modelMatrix;
        vmath::mat4 depthMVP_r = convertProjectionToImage(shadowMatrix * renderObject.modelMatrix);
        Uniformdata_s.mvpData.push_back(MVP_s);
        Uniformdata_r.mvpData.push_back(MVP_r);
        Uniformdata_r.depthMVPData.push_back(depthMVP_r);
    }

    glBindBuffer(GL_UNIFORM_BUFFER, uboBlock_s);
    GLvoid *m_data = (GLvoid *)(Uniformdata_s.mvpData.data());
    glBufferData(GL_UNIFORM_BUFFER, Uniformdata_s.mvpData.size() * sizeof(vmath::mat4), m_data,
                 GL_STATIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

    glGenBuffers(1, &uboBlock_r);
    glBindBuffer(GL_UNIFORM_BUFFER, uboBlock_r);
    glBufferData(GL_UNIFORM_BUFFER,
                 (Uniformdata_r.depthMVPData.size() + Uniformdata_r.mvpData.size()) * sizeof(vmath::mat4), NULL,
                 GL_STATIC_DRAW);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

    return true;
}

void Shadowmap::shadowProgram(double currentTime, double difTime)
{
    glBindFramebuffer(GL_FRAMEBUFFER, shadowmapFramebuffer);
    glViewport(0, 0, 1000, 1000);
    glClear(GL_DEPTH_BUFFER_BIT);
    _shadowProgram.use();
    glBindVertexArray(allVao);
    glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboBlock_s);
    GLuint blockIndex = glGetUniformBlockIndex(_shadowProgram.getProgram(), "sTransform");
    if (blockIndex != ~0) {
        glUniformBlockBinding(_shadowProgram.getProgram(), blockIndex, 0);
    }
    glBindBuffer(GL_UNIFORM_BUFFER, uboBlock_s);
    _shadowProgram.setUniform1i("prime_offset", 0);
    _shadowProgram.setUniform1i("prime_vertexcnt", 36);
    glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(renderObjects.size() - 4) * 36);
    _shadowProgram.setUniform1i("prime_offset", 5760);
    _shadowProgram.setUniform1i("prime_vertexcnt", 5400);
    glDrawArrays(GL_TRIANGLES, 160 * 36, (GLsizei)4 * 5400);
}

void Shadowmap::renderProgram(double currentTime, double difTime)
{
    double time = glfwGetTime() * kUpdateTimeFactor;
    viewMatrix = vmath::lookat(vmath::vec3{ (float)(std::cos(time) * 10.0), 5.0, (float)(std::sin(time) * 10.0) }, //
                                kCameraTarget, //
                                vmath::vec3{ 0.0f, 1.0f, 0.0f });
    renderMatrix = projectionMatrix * viewMatrix;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0, 0, getWidth(), getHeight());
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBindTexture(GL_TEXTURE_2D, shadowmapTexture);
    _renderProgram.use();
    for (std::size_t index2 = 0; index2 < renderObjects.size(); ++index2) {
        vmath::mat4 MVP_r = renderMatrix * renderObjects[index2].modelMatrix;
        vmath::mat4 depthMVP_r = convertProjectionToImage(shadowMatrix *
                                                          renderObjects[index2].modelMatrix);
        Uniformdata_r.mvpData[index2] = MVP_r;
        Uniformdata_r.depthMVPData[index2] = depthMVP_r;
    }
    glBindBufferBase(GL_UNIFORM_BUFFER, 1, uboBlock_r);
    GLuint blockIndex = glGetUniformBlockIndex(_renderProgram.getProgram(), "rTransform");
    if (blockIndex != ~0) {
        glUniformBlockBinding(_renderProgram.getProgram(), blockIndex, 1);
    }
    glBindBuffer(GL_UNIFORM_BUFFER, uboBlock_r);
    GLvoid *mv_data = (GLvoid *)(Uniformdata_r.mvpData.data());
    GLvoid *d_data = (GLvoid *)(Uniformdata_r.depthMVPData.data());
    glBufferSubData(GL_UNIFORM_BUFFER, 0, Uniformdata_r.mvpData.size() * sizeof(vmath::mat4), mv_data);
    glBufferSubData(GL_UNIFORM_BUFFER, Uniformdata_r.mvpData.size() * sizeof(vmath::mat4),
                    Uniformdata_r.depthMVPData.size() * sizeof(vmath::mat4), d_data);
    glBindBuffer(GL_UNIFORM_BUFFER, 0);

    glBindVertexArray(allVao);
    glBindBufferBase(GL_UNIFORM_BUFFER, 1, uboBlock_r);
    glBindBuffer(GL_UNIFORM_BUFFER, uboBlock_r);
    _renderProgram.setUniform1i("prime_offset", 0);
    _renderProgram.setUniform1i("prime_vertexcnt", 36);
    glDrawArrays(GL_TRIANGLES, 0, ((GLint)renderObjects.size() - 4) * 36);
    _renderProgram.setUniform1i("prime_offset", 5760);
    _renderProgram.setUniform1i("prime_vertexcnt", 5400);
    glDrawArrays(GL_TRIANGLES, 160 * 36, (GLint)4 * 5400);
}

void Shadowmap::render(double currentTime, double difTime)
{
    (void)difTime;
    glEnable(GL_DEPTH_TEST);
    shadowProgram(currentTime, difTime);
    renderProgram(currentTime, difTime);
    glDisable(GL_DEPTH_TEST);
}

void Shadowmap::shutdown()
{
    glDeleteVertexArrays(1, &allVao);
    glDeleteFramebuffers(1, &shadowmapFramebuffer);
    glDeleteBuffers(1, &allVbo);
    glDeleteBuffers(1, &uboBlock_s);
    glDeleteBuffers(1, &uboBlock_r);
    glDeleteTextures(1, &shadowmapTexture);

    _shadowProgram.clear();
    _renderProgram.clear();

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);
    glUseProgram(0);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindVertexArray(0);

    glDisable(GL_DEPTH_TEST);
    renderObjects.clear();
    Uniformdata_s.mvpData.clear();
    Uniformdata_r.mvpData.clear();
    Uniformdata_r.depthMVPData.clear();
}
REGISTER_OBJECT(Node, Shadowmap)
